package de.gsi.generate;

import static java.nio.file.StandardOpenOption.*;

import java.io.IOException;
import java.io.Writer;
import java.nio.file.*;
import java.util.*;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;

/**
 * Code generator maven plugin, that performs some simple search and replace rules on template files to generate
 * helper functions and container classes for all primitive types.
 *
 * By default it looks for input files in src/main/codegen and writes the generated code to
 * target/generated-sources/codegen.
 *
 * There are two different types of template classes. Classes ending in `Gen` are are rewritten, by repeating the
 * blocks inside `//// codegen: originalType -&gt; outputType1, outputType2, ...` and `//// end codegen` for each
 * output type within the file. This is mostly useful for utility functions.
 * Classes ending in `Proto` have to start with a `//// codegen container: originalType -&gt; outputType1, ....` line
 * and for each outputType a separate class is generated, which is mostly used for data container classes.
 *
 * In general all occurrences of originalType are replaced with the output type inside of Proto classes and within
 * codegen blocks in Gen classes. For special cases there are 3 simple comment flags to control the behaviour.
 *
 * //// codegen: skip all
 * //// codegen: skip int, short, long
 * This comment at the end of a line skips all replacing on this line and just copies the line as is.
 *
 * //// codegen: returncast all
 * Adds a cast to output type after a return or assignment. Note that you might have to add parentheses.
 *
 * //// codegen: subst:float,double:fnInteger:fnFloating
 * //// codegen: subst%all%a%a &lt; 0 ? -a : a
 * Performs a simple search and replace if the type matches. The separator can be chosen arbitrarily.
 *
 * Multiple comment flags can be given by separating them with four slashes:
 * //// codegen: returncast short //// subst%all%a%a &lt; 0 ? -a : a
 *
 * @author ennerf
 * @author Alexander Krimm
 */
@Mojo(name = "generate-sources", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class CodeGenerator extends AbstractMojo {
    public static final String JAVA_FILE_SUFFIX = ".java";
    private static final String PROTO_START = "//// codegen container:";
    private static final String LINE_COMMAND = "//// codegen:";
    private static final String TYPE_CONVERSION_SEP = "->";
    private static final String LINE_COMMAND_SEP = "////";
    public static final String START_TEMPLATE = "//// codegen:";
    public static final String END_TEMPLATE = "//// end codegen";
    public static final String COMMAND_SKIP = "skip";
    public static final String COMMAND_CAST_RETURN = "returncast";
    public static final String COMMAND_SUBST = "subst";
    public static final String PROTO_UTILCLASS_SUFFIX = "Gen";
    public static final String PROTOTYPE_CLASS_SUFFIX = "Proto";
    public static final String AUTOGENERATED_WARNING_HEADER = "// This file has been generated automatically by chartfx-generate. Do not modify!\n";

    @Parameter(defaultValue = "${project}", required = true, readonly = true)
    MavenProject project;
    @Parameter(defaultValue = "${project.basedir}/src/main/codegen", readonly = true)
    String input;
    @Parameter(defaultValue = "${project.build.directory}/generated-sources/codegen", readonly = true)
    String output;

    @Override
    public void execute() throws MojoExecutionException {
        final Path inputPath = Path.of(this.input);
        final Path outputPath = Path.of(this.output);

        // Add the generated classes to the build path
        project.addCompileSourceRoot(this.output);
        if (getLog().isInfoEnabled()) {
            getLog().info("Added directory for generated sources to compile sources: " + outputPath);
        }

        // Adding other types to helper function classes (...GenBase.java)
        extendUtilityClass(inputPath, outputPath);

        // Adding new classes for different types (...DoubleProto.");java)
        addDataTypeVariants(inputPath, outputPath);
    }

    private void addDataTypeVariants(final Path inputPath, final Path outputPath) throws MojoExecutionException {
        final List<Path> sourcesTypes = getSourceFiles(inputPath, PROTOTYPE_CLASS_SUFFIX);

        for (final Path source : sourcesTypes) {
            // Create output package
            Path relativePath = inputPath.relativize(source);
            Path outputDirectory = getOutputDirectory(outputPath, relativePath);

            // Find base class to be generated
            final String sourceClassName = removeTail(source.getFileName().toString(), JAVA_FILE_SUFFIX);
            getLog().info("Generating primitive types for class " + sourceClassName);
            final String outputClassNameBase = removeTail(sourceClassName, PROTOTYPE_CLASS_SUFFIX);
            final List<String> sourceContent = getFileContents(source);
            final String[] conversion = sourceContent.stream()
                                                .filter(s -> s.trim().startsWith(PROTO_START))
                                                .map(s -> s.substring(PROTO_START.length()).trim())
                                                .findFirst()
                                                .orElseThrow(() -> new IllegalArgumentException("File does not contain start marker"))
                                                .split(TYPE_CONVERSION_SEP);
            final String inputType = conversion[0].trim();
            final String[] outputTypes = Arrays.stream(conversion[1].split(",")).map(String::trim).toArray(String[] ::new);

            // Generate output files
            for (final String outputType : outputTypes) {
                final String outputClassSuffix = outputType.equals("U") ? "Object" : capitalise(outputType);
                final String outputClassName = outputClassNameBase + outputClassSuffix;
                final Path dest = outputDirectory.resolve(outputClassName + JAVA_FILE_SUFFIX);
                try (Writer writer = Files.newBufferedWriter(dest, CREATE, WRITE, TRUNCATE_EXISTING)) {
                    writer.write(AUTOGENERATED_WARNING_HEADER);
                    writer.write("// autogenerated code for " + outputType + " - " + outputType + " from double\n");
                    writeLines(sourceContent, writer, line -> processLine(line, inputType, outputType).replace(PROTOTYPE_CLASS_SUFFIX, outputClassSuffix));
                } catch (IOException e) {
                    throw new MojoExecutionException("Error writing file", e);
                }
            }
        }
    }

    private void extendUtilityClass(final Path inputPath, final Path outputPath) throws MojoExecutionException {
        for (final Path source : getSourceFiles(inputPath, PROTO_UTILCLASS_SUFFIX)) { // loop over all prototoype classes
            // Create output package
            final Path relativePath = inputPath.relativize(source);
            final String packageName = relativePath.getParent().toString().replaceAll("(\\\\|/)", ".");
            Path outputDirectory = getOutputDirectory(outputPath, relativePath);

            // Find name to be generated
            String sourceClassName = removeTail(source.getFileName().toString(), JAVA_FILE_SUFFIX);
            String outputClassName = removeTail(sourceClassName, PROTO_UTILCLASS_SUFFIX);

            // log output
            if (getLog().isInfoEnabled()) {
                getLog().info("Generating primitive methods for helper class " + packageName + "." + sourceClassName);
            }

            // Generate output file
            Path outputFile = outputDirectory.resolve(outputClassName + JAVA_FILE_SUFFIX);
            final List<String> sourceContent = getFileContents(source);
            try (Writer writer = Files.newBufferedWriter(outputFile, CREATE, WRITE, TRUNCATE_EXISTING)) {
                // Write minimum class layout. Maybe use JavaPoet?
                writer.write(AUTOGENERATED_WARNING_HEADER);
                generateContent(sourceContent, writer, outputClassName);
            } catch (IOException e) {
                throw new MojoExecutionException("Error reading File", e);
            }
        }
    }

    private List<String> getFileContents(final Path source) throws MojoExecutionException {
        final List<String> sourceContent;
        try {
            sourceContent = Files.readAllLines(source);
        } catch (IOException e) {
            throw new MojoExecutionException("Error reading File", e);
        }
        return sourceContent;
    }

    private Path getOutputDirectory(final Path outputPath, final Path relativePath) throws MojoExecutionException {
        Path outputDirectory = outputPath.resolve(relativePath.getParent());
        try {
            Files.createDirectories(outputDirectory);
        } catch (IOException e) {
            throw new MojoExecutionException("could not create directories", e);
        }
        return outputDirectory;
    }

    private List<Path> getSourceFiles(final Path inputPath, final String protoUtilclassSuffix) throws MojoExecutionException {
        final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**" + protoUtilclassSuffix + JAVA_FILE_SUFFIX);
        final List<Path> sources;
        try {
            sources = Files.walk(inputPath).filter(matcher::matches).collect(Collectors.toList());
        } catch (IOException e) {
            throw new MojoExecutionException("Could not enumerate files", e);
        }
        return sources;
    }

    /**
     * Generate some sort of code, e.g., search/replace within some template boundary
     */
    private static void generateContent(final List<String> source, final Writer writer, final String className) throws IOException {
        final List<String> section = new ArrayList<>();
        String[] outputTypes = null;
        String inputType = null;
        for (String line : source) {
            if (line.contains(className + PROTO_UTILCLASS_SUFFIX)) {
                line = line.replace(className + PROTO_UTILCLASS_SUFFIX, className);
            }
            if (line.trim().startsWith(START_TEMPLATE)) {
                final String[] tokens = line.trim().substring(START_TEMPLATE.length()).split(TYPE_CONVERSION_SEP);
                inputType = tokens[0].trim();
                outputTypes = Arrays.stream(tokens[1].split(",")).map(String::trim).toArray(String[] ::new);
            } else if (line.trim().startsWith(END_TEMPLATE)) {
                if (outputTypes == null) {
                    section.clear();
                } else {
                    final String indentation = "    "; // TODO: get correct indentation for section
                    // output all collected lines for all types:
                    for (final String outputType : outputTypes) {
                        writeTypeSection(writer, section, inputType, indentation, outputType);
                    }
                    // reset
                    section.clear();
                    inputType = null;
                    outputTypes = null;
                }
            } else if (inputType != null && outputTypes != null) {
                section.add(line);
            }
            writer.write(line);
            writer.write("\n");
        }
    }

    private static void writeTypeSection(final Writer writer, final List<String> section, final String inputType, final String indentation, final String outputType) throws IOException {
        // write section header comment for type
        writer.write(indentation);
        writer.write("// start: ");
        writer.write(outputType);
        writer.write("\n");
        // rewrite section for current type
        for (final String currentLine : section) {
            writer.write(processLine(currentLine, inputType, outputType));
            writer.write("\n");
        }
        // write section footer comment for type
        writer.write(indentation);
        writer.write("// end: ");
        writer.write(outputType);
        writer.write("\n\n");
    }

    private static String processLine(final String currentLine, final String inputType, final String outputType) {
        final Map<String, String> substitutions = new HashMap<>();
        boolean skip = false;
        boolean returncast = false;
        // check for line modifiers //// codegen: <command> ....
        final int cmdIndex = currentLine.indexOf(LINE_COMMAND);
        if (cmdIndex >= 0) {
            String[] commands = Arrays.stream(currentLine.substring(cmdIndex + LINE_COMMAND.length() + 1).split(LINE_COMMAND_SEP))
                                        .map(String::trim)
                                        .toArray(String[] ::new);
            for (final String command : commands) {
                if (command.startsWith(COMMAND_SKIP)) {
                    skip = skip || command.substring(COMMAND_SKIP.length()).contains(outputType);
                    skip = skip || command.substring(COMMAND_SKIP.length()).contains("all");
                } else if (command.startsWith(COMMAND_CAST_RETURN)) {
                    returncast = returncast || command.substring(COMMAND_CAST_RETURN.length()).contains(outputType);
                    returncast = returncast || command.substring(COMMAND_CAST_RETURN.length()).contains("all");
                } else if (command.startsWith(COMMAND_SUBST)) {
                    final String[] tokens = command.split(String.valueOf(command.charAt(COMMAND_SUBST.length())));
                    if (tokens[1].contains(outputType) || tokens[1].contains("all")) {
                        substitutions.put(tokens[2], tokens.length < 4 ? "" : tokens[3]);
                    }
                }
            }
        }
        // apply substitutions and commands
        String result = currentLine;
        if (!skip) {
            result = currentLine.replace(inputType, outputType).replace(capitalise(inputType), capitalise(outputType));
            if (returncast) {
                result = result.replace(" = ", " = (" + outputType + ") ")
                                 .replace("return ", "return (" + outputType + ") ");
            }
            for (final Map.Entry<String, String> substitution : substitutions.entrySet()) {
                result = result.replace(substitution.getKey(), substitution.getValue());
            }
        }
        return result;
    }

    private static String capitalise(final String input) {
        if (input == null) {
            return null;
        }
        if (input.length() == 0) {
            return "";
        }
        return Character.toUpperCase(input.charAt(0)) + input.substring(1);
    }

    private static void writeLines(List<String> lines, Writer writer, UnaryOperator<String> converter) throws IOException {
        for (String line : lines) {
            writer.write(converter.apply(line));
            writer.write("\n");
        }
    }

    private static String removeTail(String string, String tail) {
        if (!string.endsWith(tail))
            throw new IllegalStateException(string + " does not end with " + tail);
        return string.substring(0, string.length() - tail.length());
    }
}
